Глава 3. Обзор языка С

 Как уже говорилось, хорошее понимание языка С представляется необходимым для успешного программирования на C++. C++Builder в полной мере поддерживает стандарт ANSI С и, кроме того, некоторые другие версии языка (Керниган & Ричи, Unix V). Мы в основном будем ориентироваться на стандарт ANSI, который, кстати, является в какой-то мере результатом “обратного воздействия” языка C++.

В этой главе мы, опираясь на короткие примеры, расскажем об элементах программы на С и синтаксисе различных его конструкций.

Элементы простой программы

Давайте немного поближе познакомимся со строением консольной программы Hello World, написанной в предыдущей главе:

#pragma hdrstop

#include <stdio.h>

#include <conio.h>

#include <condefs.h>

//---------------------------------------------

#pragma argsused

int main(int argc, char* argv[ ])

{

printf("Hello World from Console!\n");

printf("Press any key...");

getch() ;

return 0;

}

Комментарии

Строка нашей программы, начинающаяся с двух знаков дроби (//), является комментарием. В данном случае этот “комментарий” ничего не сообщает и введен только для того, чтобы визуально отметить начало определения функции. По идее комментарии служат для вставки в текст программы пояснений, позволяющих другим программистам разобраться в назначении и работе тех или иных фрагментов кода; ну и для того, чтобы помочь самому программисту вспомнить, что же он написал полгода или месяц назад.

Комментарии совершенно игнорируются при компиляции программы, поэтому они могут содержать что угодно.

Вышеприведенная строка — комментарий в стиле C++. Стандартные компиляторы С таких комментариев не допускают. В языке С комментарий начинается с комбинации дробной черты и звездочки (/*) и заканчивается теми же символами в обратном порядке (*/). Он может занимать несколько строк, а может быть вставлен в середину строки (такие случаи бывают). Комментарий в стиле C++ начинается, как уже говорилось, с двои ной дробной черты и продолжается до конца текущей строки. Язык C++ поддерживает оба типа комментариев.

Вот пример комментария в стиле С, который можно было бы поместить в самое начало исходного файла:

/ *

** Простейшая консольная программа C++Builder.

** Выводит на экран "Hello World" и ждет, пока

** пользователь не нажмет какую-нибудь клавишу.

*/

Он занимает, как видите, пять строк. А вот комментарий в стиле C++:

getch(); // Ожидание нажатия клавиши.

В данном случае комментарий размещен в конце строки и поясняет смысл расположенного в ней оператора.

Директивы # pragma

Строки исходного кода, начинающиеся со знака #, являются, как правило, директивами препроцессора, т. е. управляют обработкой текста программы еще до его передачи собственно компилятору (сюда относятся текстовые подстановки, вставка содержимого других файлов и некоторые специальные операции). Директивы #pragma в этом смысле являются исключением, поскольку они адресованы непосредственно компилятору и служат для передачи ему различных указаний. Например, #pragma argsused говорит компилятору, что следует подавить выдачу предупреждающего сообщения о том, что параметры функции main () никак в ней не используются.

Часто директивы tpragma эквивалентны некоторым установкам компилятора, задаваемым в диалоге Project Options. Например, упомянутые выше сообщения о неиспользуемых параметрах можно было бы запретить, открыв этот диалог (Project | Options... в главном меню) на странице Compiler и нажав кнопку Warnings..., после чего будет открыто окно со списком всех возможных предупреждений; в нем следует сбросить флажок напротив сообщения “Parameter 'parameter' is never used (-wpar)”.

Правда, тем самым в проекте будут запрещены все такие предупреждения, в то время как директива argsused позволяет управлять ими для каждой из функций в отдельности.

Подробнее о #pragma и других директивах мы поговорим в следующей главе.

 Директивы #include

Директива # include заменяется препроцессором на содержимое указанного в ней файла. Обычно это заголовочные файлы с расширением .h. Они содержат информацию, обеспечивающую раздельную компиляцию файлов исходного кода и корректное подключение различных библиотек. Имя файла может быть заключено либо в угловые скобки (о, меньше—больше), либо в обычные двойные кавычки (""). Эти случаи различаются порядком поиска включаемых файлов; если использованы угловые скобки, поиск будет сначала производиться в стандартных каталогах C++Builder, если кавычки — в текущем каталоге.

Функция main()

После всех директив в программе расположено определение функции па in () . Как уже говорилось, в строгом смысле любая программа на С содержит эту функцию, которая является ее входной точкой. Однако в среде Windows вместо main () часто используется WinMain () .

Функция main () — это, конечно, частный случай функции вообще. Функции являются основными “строительными блоками” программы, или подпрограммами. Они, в свою очередь, строятся из операторов, составляющих тело функции. Каждый оператор оканчивается точкой с запятой (;). В общем виде функция определяется таким образом:

Возвращаемый_ тип_ имя функции(список_ параметров)

{

// В фигурных скобках заключено тело функции,

// составленное из отдельных операторов.

тело_функции

}

Функции — единственный тип подпрограмм С, в отличие, например, от языка Pascal, который различает функции и процедуры. Под процедурой обычно понимают подпрограмму, не возвращающую никакого значения. В С формально любая функция возвращает какой-либо тип, хотя в ANSI С этот тип может быть пустым (void). В первоначальном варианте языка (Керниган & Ричи) функция, для которой возвращаемый тип не определялся, считалась возвращающей int (целое значение). Мы иногда будем называть функции С процедурами, хотя это, строго говоря, и неправильно.

В нашем случае тело функции состоит из четырех операторов, первые три из которых являются, в свою очередь, вызовами функций. Значения, возвращаемые функциями, здесь игнорируются, т. е. функции вызываются аналогично процедурам языка Pascal. Применяемые здесь функции содержатся в стандартной (исполнительной) библиотеке С.

Параметры функции main()

Параметры функции main () служат для передачи программе аргументов командной строки, т. е. имен файлов, ключей, опций и вообще всего, что вы вводите с клавиатуры после подсказки DOS, запуская программу. Конечно, программа не обязана воспринимать какие-либо команды, указываемые в строке запуска, однако в любом случае функции main () передаются два параметра — число аргументов/включая имя, под которым запущена программа (argc), и массив указателей (argv) на отдельные аргументы (выделенные элементы командной строки). Забегая вперед, приведем пример, который распечатывает по отдельности все “аргументы” строки, введенной пользователем при запуске:

#include <stdio.h>

int main(int argc, char *argv[])

{

int i;

for (i=0; i<argc; i++)

printf ( "%s\n", argv[i]);

return 0;

}

Вы сможете вернуться к этому примеру, когда мы изучим массивы, строки и циклы. Теперь мы займемся более последовательным изучением основ языка С.

Представление данных в С

Любая программа так или иначе обрабатывает данные. Наша маленькая программа обрабатывает свои данные — строку сообщения “Hello World”, выводя ее на экран. Рассмотрим, какие возможны варианты представления информации в С.

Литералы

Прежде всего, данные могут присутствовать непосредственно в тексте программы. В этом случае они представляются в виде литеральных констант. Эти литералы могут быть числовыми, символьными и строковыми. В программе Hello World мы пользовались строковыми литералами. Это — последовательность символов, заключенная в двойные кавычки.

Символьный литерал служит для представления одиночного знака. Это символ, заключенный в одиночные кавычки (апострофы).

Числовые литералы могут быть вещественными (с плавающей точкой) и целыми. В случае целого литерала он может быть записан в десятичной, восьмеричной или шестнадцатеричной нотации (системе счисления). Вещественный литерал записывается либо в обычной десятичной, либо в экспоненциальной нотации.

В таблице 3.1 перечислены все упомянутые выше виды литеральных констант и даны соответствующие примеры.

Таблица 3.1. Литеральные константы

Литерал

Описание

Примеры

Символьный

Одиночный символ, заключенный в апострофы

'W', '&', 'Ф'

Строковый

Последовательность символов, заключенная в обычные (двойные) кавычки

"Это строка \n"

Целый

Десятичный — последовательность цифр, не начинающаяся с нуля

123, 1999

 

 

Восьмеричный — последовательность цифр от нуля до семерки, начинающаяся с нуля

011, 0177

 

 

Шестнадцатеричный — последовательность шестнадцатеричных цифр (0 - 9 и А - F), перед которой стоит 0X или Оx

ОХ9А, Oxffff

Вещественный

Десятичный — [цифры].[цифры]

123., 3.14, .99

 

 

Экспоненциальный — [цифры]Е|е[+|-] цифры

Зе-10, 1.17е6

 

Можно дать литеральной константе некоторое имя, определив ее в качестве макроса препроцессора. После этого можно вместо литерала использовать имя. Это особенно удобно в том случае, когда одна и та же константа встречается в различных частях программы; используя имя вместо литералов, вы гарантированы от опечаток и, кроме того, гораздо проще вносить в код изменения, если значение константы нужно модифицировать. Макросы определяются директивой препроцессора #define:

#define PI 3.14159265

#define TRUE 1

#define FALSE 0

При обработке исходного кода препроцессором выполняется просто текстовая подстановка: каждое вхождение имени макроса заменяется соответствующим ему литералом. Макросы называют также символическими константами (не путайте с символьными).

Встроенные типы данных

Однако данные могут не только вписываться в текст программы, но и храниться в памяти во время ее выполнения. С физической точки зрения любая информация в памяти машины выглядит одинаково — это просто последовательности нулей и единиц, сгруппированных в байты. Поэтому наличные в памяти данные должны как-то интерпретироваться процессором; этой интерпретацией управляет, естественно, компилятор С. Любая информация рассматривается компилятором как принадлежащая к некоторому типу данных. В языке имеется несколько встроенных, или простых, типов (возможны и другие типы данных, например, определяемые пользователем). Простые типы перечислены в следующей таблице.

Таблица 3.2. Встроенные типы данных

Тип данных

Размер (бит)

Диапазон

char

8

-128 - 127

signed char

8

-128 - 127

unsigned char

8

0 - 255

short

16

-32768 - 32767

unsigned short

16

0 - 65535

int

32

-2147483648 - 2147483647

unsigned int

32

0 - 4294967295 .

long

32

-2147483648 - 2147483647

unsigned long

32

0 - 4294967295

float

32

3.410-38 - 3.41038

double

64

1.71010-308 - 1.710308

long double

80

3.410-4932 - 3.4104932

 

Может быть, стоит напомнить, что отрицательные целые числа представляются в машине в форме дополнения до двух. Чтобы изменить знак числа на противоположный, нужно инвертировать все его разряды (0 заменить на 1 и наоборот и прибавить к полученному числу единицу. Например, взяв +1 типа char (00000001), инвертировав все биты (11111110) и прибавив 1, мы получим -1 (11 111 111).

Ключевые слова short, long и unsigned являются, строго говоря, модификаторами для типа int. Однако допускается сокращенная запись. Так, unsigned short — на самом деле сокращение для unsigned short int.

Следует, вероятно, повторить, что мы говорим здесь о C++Builder 5, т. е. 32-разрядном компиляторе. Размер и допустимый диапазон значений приведены именно для данного случая. Поэтому, например, тип int имеет размер 32 бита (4 байта) и эквивалентен типу long; на 16-разрядной машине int имел бы размер 2 байта, как short. О таких вещах не следует забывать, особенно если вы занимаетесь переносом программ на машину с другой разрядностью.

Переменные

Итак, отдельная единица данных должна обязательно иметь определенный тип. Для ее хранения во время работы программы мы должны, во-первых, отвести соответствующее место в памяти, а во-вторых, идентифицировать ее, присвоив некоторое имя. Именованная единица памяти для-хранения данных называется переменной.

Переменные создаются с помощью оператора объявления переменных, в котором указывается тип, имена переменных и (при необходимости) начальные значения, которыми переменные инициализируются. Вот несколько примеров:

short i;

// Объявление короткой целой

// переменной.

char quit = 'Q';

// Инициализация символьной

// переменной.

float fl, factor = 3.0, f2;

// Три переменных типа float,

// одна из которых

// инициализируется.

Синтаксис оператора объявления можно описать примерно так:

тип имя_переменной [= инициализирующее_значение][, ...];

Как и любой другой оператор С, он оканчивается точкой с запятой.

Имена в С могут состоять из букв латинского алфавита, цифр и символов подчеркивания, причем первый символ имени не может быть цифрой. Следует помнить, что компилятор С различает регистр (прописные и строчные буквы). Таким образом, имена aVariable и AVariable считаются различными.

Инициализирующее значение должно быть литеральной (или символической) константой либо выражением, в которое входят только константы. Инициализация происходит при создании переменной, один раз за все время ее существования (об этом мы будем говорить ниже).

Объявление переменной должно предшествовать ее использованию в программе. Обычно все объявления размещают в начале тела функции или блока, до всех исполняемых операторов.

 

Типизированные константы

Разновидностью переменных являются типизированные константы. Это переменные, значение которых (заданное при инициализации) нельзя изменить. Создание типизированной константы ничем не отличается от инициализации переменной, за исключением того, что перед оператором объявления ставится ключевое слово const:

const тип имя_константы = значение [, ...];

Например:

const double Pi = 3.14159265;

Ранее мы демонстрировали определение символической константы:

#define PI 3.14159265

Чем этот макрос отличается от показанной выше типизированной константы? Здесь следует иметь в виду два момента. Во-первых, типизированная константа по своему смыслу относится к конкретному типу данных, поэтому компилятор генерирует совершенно определенное представление для ее значения. Представление символической константы не определено.

Во-вторых, имя символической константы значимо только на этапе препроцессор ной обработки исходного кода, поэтому компилятор не включает ею в отладочную информацию объектного модуля. Вы не можете использовать это имя в выражениях при отладке. Напротив, типизированные константы являются по существу переменными, и их имена доступны отладчику. В силу этих причин предпочтительнее применять для представления постоянных величин типизированные константы, а не макросы #define.

Операции и выражения

Как все знают, из переменных, функций и констант в алгебре можно составлять формулы. Точно так же и в языке C++ следующим уровнем представления данных после одиночных переменных и констант являются своего рода формулы, называемые выражениями.

Единственное отличие выражений C++ от конвенциональных формул заключается в том, что набор операций, соединяющих члены выражения, отличается от применяемого, скажем, в алгебре. Вот один пример выражения:

aResult = (first - second * RATE) <<3

Операции характеризуются своим приоритетом, определяющим порядок, в котором производится оценка выражения, и правилом ассоциации, задающим направление последовательных оценок идущих друг за другом операций одного приоритета.

Как и в обычных формулах, для изменения порядка оценки выражения могут применяться круглые скобки (кстати, в приведенном выражении они излишни и введены только для наглядности). Знак равенства здесь также является операцией присваивания, которая сама (и, соответственно, все выражение в целом) возвращает значение. В этом отличие С от других языков, в частности Pascal, где присваивание является оператором а не операцией. Оператором выражение станет, если поставить после него точку с запятой.

В следующей таблице дана сводка всех операций языка С в порядке убывания приоритета.

Таблица 3.3. Операции языка С

Операция

Описание

Приоритет

Ассоциация

Первичные и постфиксные операции

[]

индексация массива

16

слева направо

()

вызов функции

16

слева направо

.

элемент структуры

16

слева направо

->

элемент указателя

16

слева направо

++

постфиксный инкремент

15

слева направо

--

постфиксный декремент

15

слева направо

Одноместные операции

++

префиксный инкремент

14

справа налево

--

префиксный декремент

14

справа налево

sizeof

размер в байтах

14

справа налево

(тип)

приведение типа

14

справа налево

~

поразрядное NOT

14

справа налево

!

логическое NOT

14

справа налево

-

унарный минус

14

справа налево

&

взятие адреса

14

справа налево

*

разыменование указателя

14

справа налево

Двухместные и трехместные операции

Мультипликативные

*

умножение

13

слева направо

/

деление

13

слева направо

%

взятие по модулю

13

слева направо

Аддитивные

+

сложение

12

слева направо

-

вычитание

12

слева направо

Поразрядного сдвига

<<

сдвиг влево

11

слева направо

>>

сдвиг вправо

11

слева направо

Отношения

<

меньше

10

слева направо

<=

меньше или равно

10

слева направо

>

больше

10

слева направо

>=

больше или равно

10

слева направо

==

равно

9

слева направо

Операция

Описание

Приоритет

Ассоциация

! =

не равно

9

слева направо

Поразрядные

&

поразрядное AND

8

слева направо

^

поразрядное XOR

7

слева направо

|

поразрядное OR

6

слева направо

Логические

&&

логическое AND

5

слева направо

||

логическое OR

4

слева направо

Условные

? :

условная операция

3

справа налево

Присваивания

=

присваивание

2

справа налево

*=

присвоение произведения

2

справа налево

/=

присвоение частного

2

справа налево

%=

присвоение модуля

2

справа налево

+=

присвоение суммы

2

справа налево

-=

присвоение разности

2

справа налево

<<=

присвоение левого сдвига

2

справа налево

>>=

присвоение правого сдвига

2

справа налево

&=

присвоение AND

2

справа налево

^=

присвоение XOR

2

справа налево

|=

присвоение OR

2

справа налево

,

запятая

1

слева направо

 

Семантика операций

Несколько слов об операциях, перечисленных в таблице (всего их получилось что-то около пятидесяти). Смысл некоторых из них будет проясняться в дальнейшем при изучении массивов, структур и указателей; здесь же мы вкратце расскажем об операциях, относящихся в основном к арифметике.

Арифметические операции

К арифметическим мы отнесем те операции, которые перечислены в таблице под рубриками “Мультипликативные” и “Аддитивные”. Нужно сказать, что только эти операции (да и то за исключением взятия по модулю) имеет смысл применять к вещественным операндам (типам float, double и long double). Для таких операндов все обстоит вполне понятным и конвенциональным образом; это обычные умножение, деление, сложение и вычитание.

Операция взятия по модулю применяется только к целочисленным операндам (char, short, int. long) и дает остаток от деления первого операнда на второй. Специальной операции деления нацело в С нет — для него применяется обычная операция деления (/). Если оба операнда ее являются целыми, то результат этой операции также будет целым, равным частному от деления с остатком первого операнда на второй.

В качестве предостережения заметим, что это свойство деления в С часто бывает источником ошибок даже у довольно опытных программистов. Предположим, некто хочет вычислить объем шара и, не долго думая, пишет, переводя известную формулу на язык С:

volume = 4/3 * Pi * r*r*r;

Все операции в выражении правой части имеют одинаковый приоритет, и оценка выражения производится в последовательности слева направо. На первом шаге производится деление 4/3, но это будет делением нацело с результатом, равным 1. Эта единица преобразуется далее в вещественное 1.0 (возведение типа, описанное ниже), а дальше все идет как положено. Коэффициент в формуле, таким образом, получается равным 1.0 вместо ожидаемого 1.333...

Операции присваивания

Операция присваивания (=) не представляет особых трудностей. При ее выполнении значением переменной в левой части становится результат оценки выражения справа. Как уже говорилось, эта операция сама возвращает значение, что позволяет, например, написать:

а = b = с = someExpression;

После исполнения такого оператора все три переменных а, b, с получат значение, равное someExpression. Что касается остальных десяти операций присваивания, перечисленных в таблице, то они просто служат для сокращенной нотации присваивании определенного вида. Например,

s += i;

эквивалентно

s = s + i;

Другими словами, оператор вроде

х *= 10;

означает “присвоить переменной х ее текущее значение, умноженное на 10”.

Присваивание — единственная операция, меняющая содержимое одного из своих операндов (если не считать специальные операции инкремента и декремента, описанные ниже).

Приведение типа

Если в операторе присваивания тип результата, полученного при оценке выражения в правой части, отличен от типа переменной слева, компилятор выполнит автоматическое приведение типа (по-английски typecast или просто cast) результата к типу переменной. Например, если оценка выражения дает вещественный результат, который присваивается целой переменной, то дробная часть результата будет отброшена, после чего будет выполнено присваивание. Ниже показан и обратный случай приведения:

int p;

double pReal = 2.718281828;

 

p = pReal; // p получает значение 2

pReal = p; // pReal теперь равно 2.0

Возможно и принудительное приведение типа, которое выполняется посредством операции приведения и может применяться к любому операнду в выражении, например:

р = рО + (int)(pReal + 0.5); // Округление pReal

Следует иметь в виду, что операция приведения типа может работать двояким образом. Во-первых, она может производить дейсгвительное преобразование данных, как это происходит при приведении целого типа к вещественному и наоборот. Получаются совершенно новые данные, физически отличные от исходных. Во-вторых, операция может никак не воздействовать на имеющиеся данные, а только изменять их интерпретацию. Например, если переменную типа short со значением -1 привести к типу unsigned short, то данные останутся теми же самыми, но будут интерпретироваться по-другому (как целое без знака), в результате чего будет получено значение 65535.

Смешанные выражения

В арифметическом выражении могут присутствовать операнды различных типов — как целые, так и вещественные, а кроме того, и те и другие могут иметь различную длину (short, long и т. д.), в то время как оба операнда любой арифметической операции должны иметь один и тот же тип. В процессе оценки таких выражений компилятор следует алгоритму т. н. возведения типов, который заключается в следующем.

На каждом шаге оценки выражения выполняется одна операция и имеются два операнда. Если их тип различен, операнд меньшего “ранга экстенсивности” приводится к типу более “экстенсивного”. Под экстенсивностью понимается диапазон значений, который поддерживается данным типом. По возрастанию экстенсивности типы следуют в очевидном порядке:

char short

int, long

float

double

long double

Кроме того, если в операции участвуют знаковый и без знаковый целочисленные типы, то знаковый операнд приводится к без знаковому типу. Результат тоже будет без знаковым. Во избежание ошибок нужно точно представлять себе, что при этом происходит, и при необходимости применять операцию приведения, явно преобразующую тот или иной операнд.

Некоторые считают (я даже встречал это в книгах), что в выражении все операнды заранее приводятся к наиболее экстенсивному типу, а уж потом производится оценка. Это, конечно, не так. Возведение типов выполняется последовательно для каждой текущей пары операндов. (См. пример в замечании к параграфу “Арифметические операции”.)

Логические операции и операции отношения

В языке С нет специального логического или булева типа данных. Для представления логических значений используются целочисленные типы. Нулевое значение считается ложным (false), любое ненулевое — истинным (true).

Операции отношения служат для сравнения (больше — меньше) или проверки на равенство двух числовых операндов. Операции возвращают “логическое” значение, т. е. ненулевое целое в случае, если условие отношения удовлетворяется, и нулевое в противном случае.

Логические операции и отношения мы рассмотрим подробнее, когда будем обсуждать управляющие структуры С.

Поразрядные операции и сдвиги

Эти операции применяются к целочисленным данным. Последние рассматриваются просто как набор отдельных битов.

При поразрядных операциях каждый бит одного операнда комбинируется (в зависимости от операции) с одноименным битом другого, давая бит результата. При единственной одноместной поразрядной операции — отрицании (~) — биты результата являются инверсией соответствующих битов ее операнда.

При сдвиге влево биты первого операнда перемещаются влево (в сторону старших битов) на заданное вторым операндом число позиций. Старшие биты, оказавшиеся за пределами разрядной сетки, теряются; справа результат дополняется нулями.

Результат сдвига вправо зависит от того, является ли операнд знаковым или без знаковым. Биты операнда перемещаются вправо на заданное число позиций. Младшие биты теряются. Если операнд — целое со знаком, производится расширение знакового бита (старшего), т. е. освободившиеся позиции принимают значение 0 в случае положительного числа и 1 — в случае отрицательного. При без знаковом операнде старшие биты заполняются нулями.

Сдвиг влево эквивалентен умножению на соответствующую степень двойки, сдвиг вправо — делению. Например,

aNumber = aNumber <<4;

умножает aNumber на 16.

Инкремент и декремент

Операции инкремента (++) и декремента (--) соответственно увеличивают или уменьшают свой операнд (обязательно переменную) на единицу. Они изменяют значение самой переменной, т. е. являются скрытыми присваиваниями. Иногда эти операции применяют в качестве самостоятельного оператора:

i++; или ++i;

И то и другое эквивалентно

i = i + 1;

Но эти операции могут использоваться и в выражениях:

sum - sum + х * --i;

Инкремент и декремент реализуются в двух формах: префиксной (++i) и постфиксной (i--). Префиксные операции выполняются перед тем, как будет производиться оценка всего выражения. Все постфиксные операции выполняются уже после оценки выражения, в которое они входят.

Условная операция

Условная операция (? :) позволяет составить условное выражение, т. е. выражение, принимающее различные значения в зависимости от некоторого условия. Эта операция является трехместной. Если ее условие (первый операнд) истинно, оценкой выражения будет второй операнд; если ложно — третий. Классический пример:

max_ab = b > b? а : b;

Запятая

Помимо того, что запятая в С служит разделителем различных списков (как в списке параметров функции), она может использоваться и как операция. Запятая в этом качестве также является разделителем, но обладает некоторыми дополнительными свойствами.

Везде, где предполагается выражение, может использоваться список выражений, возможно, заключенный в скобки (так как операция-запятая имеет наинизший приоритет). Другими словами,

Выражение1, выражение2[, ...]

также будет выражением, оценкой которого является значение последнего элемента списка. При этом операция-запятая гарантирует, что оценка выражений в списке будет производиться по порядку слева направо. Вот два примера с операцией-запятой:

i++, j++;

// Значение выражения

// игнорируется.

res = (j = 4, j += n, j++);

// res присваивается n + 4.

// j равно n + 5.

Операция-запятая применяется довольно редко, обычно только в управляющих выражениях циклов.

Функции

Функция, как уже говорилось, является основным структурным элементом языка С. Выше мы уже показывали синтаксис определения функции:

возвращаемый_тип имя_функции(список_параметров)

{

тело_функции

} . .

Мы уже не раз пользовались подобной формой описания синтаксиса. Это нечто вроде метаязыка (первый из них назывался формой Бэкуса-Наура), которые широко используются для формального определения языков программирования. Надеемся, что смысл написанного достаточно ясен. Курсивом_без_ пробелов обозначаются синтаксические элементы, имеющие самостоятельное значение. Например, список_параметров является отдельной синтаксической единицей, хотя он обладает собственной внутренней структурой. Можно было бы раскрыть его определение примерно так:

Список__параметров:

Void

объявление_параметра[, объявление_параметра...]

Далее требовалось бы раскрыть смысл элемента объявление_параметра и т. д. (Несколько строк под определяемым понятием показывают различные варианты его раскрытия.)

Необязательные элементы помещаются в квадратные скобки. Взаимоисключающие варианты отделяются друг от друга вертикальной чертой (например, [+ | -] означает: “здесь может стоять либо плюс, либо минус, либо вообще ничего"). Многоточие показывает, что последний синтаксический элемент может повторяться произвольное число раз.

Хотя такая форма описания синтаксиса не вполне корректна, она, как нам кажется, интуитивно ясна и будет в некоторых случаях довольно полезной.

Тело функции состоит из операторов, каждый из которых завершается точкой с запятой. (В отличие, скажем, от языка Pascal точка с запятой является в С элементом оператора, а не разделителем.) Заметьте, что сам заголовок функции (его иногда называют сигнатурой) не содержит точки с запятой.

Оператор в С не обязан располагаться в одной строке. Он может занимать и несколько строк; переход на следующую строку с точки зрения компилятора эквивалентен простому пробелу. Говоря точнее, перевод строки является одним из пробельных символов (whitespace), таких, как пробел, табуляция и переход на новую страницу. Пробельный символ может быть вставлен между любыми соседними лексическими элементами С.

Помимо определения для функции обычно пишется также ее объявление, или прототип, который размещается в заголовочном файле с расширением .h и служит для проверки корректности обращений к функции при раздельной компиляции исходных файлов. Об этом мы уже рассказывали в 1-й главе. Прототип идентичен заголовку функции, но заканчивается точкой с запятой. Тело функции отсутствует:

возвращаемый тип имя функции(список параметров);

Функции пишутся для того, чтобы можно было их вызывать в различных местах программы. Вызов функции является выражением и принадлежит к типу, указанному в ее определении; он имеет вид

имя_функции(параметры)

параметры:

пусто

параметр[, параметр...]

Параметры, используемые при вызове функции, часто называют аргументами.

Значение, возвращаемое функцией, можно игнорировать, т. е. использовать функцию в качестве процедуры:

DoSomething(argi, arg2);

Мы так и поступали, когда выводили на экран сообщения функцией

printf().

Функции, “возвращающие значение” типа void, могут вызываться только таким образом. С другой стороны, возвращаемое функцией значение можно использовать в выражениях наряду с переменными и константами:

aResult = 1. - cos(arg);

Функция в С может иметь переменное или, точнее, неопределенное число параметров. В этом случае за последним обязательным параметром в заголовке функции следует многоточие (...). Подобным образом объявляется функция printf:

int printf(const char *format, ...);

Неопределенное число параметров означает, что количество и тип действительных аргументов в вызове должно так или иначе ей сообщаться, как это и происходит в случае printf () — там число аргументов определяется по числу спецификаторов в строке формата (см. следующий параграф). Тело функции с переменным числом параметров должно быть реализовано на языке ассемблера или, возможно, при помощи каких-то не вполне “законных” ухищрений.

Пока мы имели дело всего с тремя функциями: main (), printf () и getch () . Давайте поближе познакомимся с printf () и другими функциями ввода-вывода стандартной библиотеки С.

Ввод и вывод в С

printf () является функцией стандартной библиотеки с переменным числом аргументов. Она всегда имеет по крайней мере один аргумент — строку формата, чаще всего строковый литерал. Строка может содержать спецификаторы преобразования. Функция сканирует строку и передает ее символы на стандартный вывод программы, по умолчанию консоль, пока не встретит спецификатор преобразования. В этом случае printf () ищет дополнительный аргумент, который форматируется и выводится в соответствии со спецификацией. Таким образом, вызов printf () должен содержать столько дополнительных аргументов, сколько спецификаторов преобразования имеется в строке формата.

Спецификация преобразования

Синтаксис спецификатора преобразования имеет такой вид:

%[флаги] [поле][.точность][размер]символ типа

Как видите, обязательными элементами спецификатора являются только начальный знак процента и символ, задающий тип преобразования. Следующая таблица перечисляет возможные варианты различных элементов спецификации.

Таблица 3.4. Элементы спецификатора преобразования

Элемент

Символ

Аргумент

Описание

флаг

-

 

 

Выровнять вывод по левому краю поля.

 

 

0

 

 

Заполнить свободные позиции нулями вместо пробелов.

 

 

+

 

 

Всегда выводить знак числа.

 

 

пробел

 

 

Вывести пробел на месте знака, если число положительное.

 

 

#

 

 

Вывести 0 перед восьмеричным или Ох перед шестнадцатеричным значением.

поле

число

 

 

Минимальная ширина поля вывода.

точность

число

 

 

Для строк — максимальное число выводимых символов; для целых — минимальное число выводимых цифр; для вещественных — число цифр дробной части.

размер

h

 

 

Аргумент -- короткое целое.

 

 

1

 

 

Аргумент — длинное целое.

 

 

L

 

 

Аргумент имеет тип long double.

Элемент

Символ

Аргумент

Описание

символ типа

d

целое

Форматировать как десятичное целое со знаком.

 

 

i

целое

То же, что и d.

 

 

о

целое

Форматировать как восьмеричное без знака.

 

 

U

целое

Форматировать как десятичное без знака.

 

 

х

целое

Форматировать как шестнадцатеричное в нижнем регистре.

 

 

Х

целое

Форматировать как шестнадцатеричное в верхнем регистре.

 

 

f

вещественное

Вещественное в форме [-]dddd.dddd.

 

 

е

вещественное

Вещественное в форме [-]d.dddde[+|-]dd.

 

 

Е

вещественное

То же, что и е, с заменой е на Е.

 

 

ё

вещественное

Использовать форму f или е в зависимости от величины числа и ширины поля.

 

 

G

вещественное

То же, что и g — но форма f или Е.

 

 

с,

символ

Вывести одиночный символ.

 

 

s

строка

Вывести строку.

 

 

п

указатель

Аргумент — указатель на переменную типа int. В нее записывается количество выведенных к данному моменту символов.

 

 

р

указатель

Вывести указатель в виде шестнадцатеричного числа ХХХХХХХХ.

 

Как видите, флаги задают “стиль” представления чисел на выводе, поле и точность определяют характеристики поля, отведенного под вывод аргумента, размер уточняет тип аргумента и символ_типа задает собственно тип преобразования. Следующий пример показывает возможности форматирования функции printf () . Советую не полениться и поэкспериментировать с этим кодом, меняя флаги и параметры поля вывода.

Листинг 3.1. Возможности функции printf ()

/*

** Printf.с: Демонстрация форматирования вывода на консоль

** функцией printf().

* /

#pragma hdrstop

#include <stdio.h>

#include <conio.h>

#pragma argsused

int main(int argc, char *argv[])

{

double p = 27182.81828;

int j = 255;

char s[] = "Press any key...";

/* Вывести 4 цифры; вывести обязательный знак: */

printf("Test integer formatting: %13.4d %4-8d\n", j, j);

/* Вывести по левому краю со знаком; заполнить нулями: */ printf("More integer formatting: %-+13d % 08d\n", j, j);

printf("Test octal and hex: %#13o %#8.6x\n", j, j);

printf("\nTest e and f conversion: %13.7e %8.2f\n", p, p) ;

printf("\n%s", s); /* Вывести строку подсказки. */

getch () ;

return 0;

}

Рис. 3.1 Тестирование функции printf()

Прием, позволяющий компилировать программу с главным файлом .с (а не .срр), описан в предыдущей главе в замечании к разделу о консольных приложениях. Конечно, показанная программа будет компилироваться и в режиме C++.

Escape-последовательности

В строках языка С для представления специальных (например, непечатаемых) символов используются escape-последователъности, состоящие из обратной дробной черты, за которой следует один или несколько символов. (Название появилось по аналогии с командами управления терминалом или принтером, которые действительно представляли собой последовательности переменной длины, начинающиеся с кода ESC.) В приведенных примерах функции printf () вы уже встречались с одной такой последовательностью — \n. Сама обратная косая черта называется escape-символом..

В таблице 3.5 перечислены возможные esc-последовательности.

Таблица 3.5. Escape-последовательности языка С

Последовательность

Название

Описание

Звонок

Подает звуковой сигнал.

\b

Возврат на шаг

Возврат курсора на одну позицию назад.

\f

Перевод страницы

Начинает новую страницу.

\n

Перевод строки

Начинает новую строку.

\r

Возврат каретки

Возврат курсора к началу текущей строки.

\t

Табуляция

Переход к следующей позиции табуляции.

\v

Вертикальная табуляция

Переход на несколько строк вниз.

\\

 

 

Выводит обратную дробную черту.

\'

 

 

Выводит апостроф (одинарную кавычку).

\"

 

 

Выводит кавычку (двойную).

 

Кроме того, esc-последовательности могут представлять символы в ASCII-коде — в восьмеричном или шестнадцатеричном формате:

\000

От одной до трех восьмеричных цифр после esc-символа.

\хНН или \ХНН

Одна или две шестнадцатеричных цифры после esc-символа.

 

Функции ввода строки — scanf() и gets()

В языке С для ввода имеется “зеркальный двойник” printf() — функция scant (). Функция читает данные со стандартного ввода, по умолчанию — клавиатуры. Она так же, как и printf () , принимает строку формата с несколькими спецификаторами преобразования и несколько дополнительных параметров, которые должны быть адресами переменных, куда будут записаны введенные значения.

В языке С функция не может изменять значение передаваемых ей аргументов, поскольку ей передается только временная копия содержимого соответствующей переменной. Это называется передачей параметра.по значению. В языке Pascal возможна также передача по ссылке, позволяющая функции изменить саму переменную-аргумент. Параметр, передаваемый по ссылке, объявляется с ключевым словом var. В С нет автоматического механизма передачи по ссылке. Чтобы передать из функции некоторое значение через параметр, ее вызывают с указателем на переменную (грубо говоря, ее адресом), подлежащую модификации. Функция не может изменить переданный ей аргумент, т. е. сам адрес, но она может записать информацию в память по этому адресу. Адрес получают с помощью операции &, например, SaVar. Подробнее мы обсудим это, когда будем говорить об указателях.

Примером вызова scanf () может служить следующий фрагмент кода:

int age;

printf("Enter your age: "); //' Запросить ввод возраста

// пользователя. scanf ("%d", &age); // Прочитать введенное число.

Функция возвращает число успешно сканированных полей, которое в приведенном фрагменте игнорируется. При необходимости вы можете найти полную информацию по scanf () в оперативной справке C++Builder. Однако следует сказать, что программисты не любят эту функцию и пользуются ей очень редко. Причина в том, что опечатка при вводе (скажем, наличие буквы в поле, предполагающем ввод числа и т. п.) может привести к непредсказуемым результатам. Контролировать корректность ввода и обеспечить адекватную реакцию программы на ошибку при работе со scanf () довольно сложно. Поэтому часто предпочитают прочитать целиком всю строку, введенную пользователем, в некоторый буфер, а затем самостоятельно декодировать ее, выделяя отдельные лексемы и преобразуя их в соответствующие значения. В этом случае можно контролировать каждый отдельный шаг процесса преобразования.

Ввод строки с клавиатуры производится функцией gets ():

char s[80] ;

gets (s) ;

Буфером, в который помещается введенная строка, является здесь символьный массив s [ ]. О массивах чуть позже, пока же скажем, что в данном случае определяется буфер, достаточный для хранения строки длиной в 79 символов — на единицу меньше, чем объявленная длина массива. Одна дополнительная позиция необходима для хранения признака конца строки; все строки в С должны оканчиваться нуль-символом \0, о котором программисту, как правило, заботиться не нужно. Функции обработки строк сами распознают эти символы или, как gets (), автоматически добавляют нуль-символ в конец строки-результата. Функция gets () возвращает данные через параметр, поэтому, как говорилось выше, ей нужно передать в качестве параметра адрес соответствующего символьного массива. Операция взятия адреса, однако, здесь не нужна, поскольку имя массива (без индекса) само по себе является указателем на начало массива. Забегая вперед, скажем, что показанная нотация эквивалентна

gets(&s[0]);

// Аргумент - указатель на начальный элемент

// массива s.

Для преобразования строк, содержащих цифровое представление чисел, в численные типы данных могут применяться функции atoi(), ato1 () и atof (). Они преобразуют строки соответственно в целые, длинные целые и вещественные числа (типы int, long и double). Входная строка может содержать начальные пробелы; первый встреченный символ, который не может входить в число, завершает преобразование. Прототипы этих функций находятся в файле stdlib.h.

Пример функции

Теперь мы напишем небольшую программу, которая проиллюстрирует все существенные моменты создания функции; в программе применяются некоторые из функций для работы со строками, описанных выше.

Пользователю предлагается ввести имя (в произвольной форме — только имя, имя и фамилию и т. п.), а затем номер телефона, просто как 7-значное число без пробелов или дефисов. После этого программа распечатывает полученные данные, выводя номер телефона в более привычном формате (рис. 3.2).

Листинг 3.2. Пример создания функции

/*

** Convert.с: Пример функции, преобразующей число

** в строку специального вида.

*/

#pragma hdrstop

#include <stdio.h>

#include <stdlib.h>

#include <conio.h>

/* Прототип функции */

void Convert(char *buffer, long num);

//-----------------------------------------

#pragma argsused

int main(int argc, char* argv[])

{

long number;

char s[80], name[80] ;

printf("Enter name: ");

gets(name) ;

printf("Enter phone number: ");

gets (s) ;

number = atol(s);

/* Преобразовать номер в обычную форму. */

Convert(s, number);

/* Вывести результат. */

printf("\n%-30s %10s\n", name, s);

getch () ;

return 0;

}

/* Определение функции */

void Convert(char *buffer, long num)

{

int grp1, grp2, grp3;

grp3 = num % 100; // Две последние цифры.

num /= 100;

grp2 = num % 100; // Две средние цифры

grp1 = num / 100; // Три старшие цифры /* Преобразовать в строку. */ sprintf (buffer, "%03d-%02d-%02d", grp1, grp2, grp3) ;

}

Функция Convert () описана как void и не возвращает значения, вследствие чего в ее теле можно опустить оператор return. (Я, кажется, еще не говорил, что именно этот оператор служит для возврата значения функции в вызывающую программу.) Она преобразует переданный ей телефонный номер (второй параметр) и записывает его в указанный строковый буфер (первый параметр). Центральный момент преобразования — разбиение номера на группы — является довольно характерным примером применения операций деления с остатком.

Рис. 3.2. Программа Convert

Для преобразования полученных групп в строку вызывается функция sprintf () . Она совершенно аналогична функции printf () за исключением того, что вместо вывода на консоль записывает результат в строковый буфер, указанный первым параметром.

В основной программе, т. е. в функции main (), использована функция ato1 (), преобразующая строку в длинное целое.

В верхней части файла мы поместили прототип функции Convert () Определение функции мы поместили после main(), поэтому прототип в данном случае необходим — без него компилятор не сможет корректно генерировать вызов Convert () .

Подытожим некоторые правила относительно прототипов и определений функций:

 

Область действия переменных и связанные с ней понятия

Теперь, когда мы более-менее разобрались с принципами функциональной организации программы, следует обсудить некоторые весьма важные вопросы относительно переменных.

Переменные в С могут быть локальными и глобальными, статическими и автоматическими, регистровыми, внешними и даже нестабильными. Они различаются своей областью действия, видимостью и временем жизни. Попробуем как-то сориентироваться во всем этом многообразии.

Область действия

Область действия — это та часть программы, где переменная в принципе доступна для программного кйда (что означает это “в принципе”, выяснится чуть позже). По своей области действия переменные делятся на локальные и глобальные.

Локальные переменные объявляются внутри функции и вне ее тела недоступны. Вернитесь к последнему примеру. Там программа состоит из двух функций. В main () объявляются переменные number, s и name (две последних — не простые переменные, а массивы, но это несущественно). В функции Convert объявлены grp1, grp2 и grp3.

Все эти переменные являются локальными. К каждой из них можно обращаться только в пределах объявляющей ее функции. Поэтому, кстати, имена локальных переменных не обязаны быть уникальными. Если две функции описывают переменную с одним и тем же именем, то это две совершенно различные переменные и никакой неоднозначности не возникает.

Параметры в определении функции (формальные параметры) можно рассматривать как локальные переменные, инициализируемые значениями аргументов при ее вызове.

В противоположность локальным глобальные переменные не относятся ни к какой функции и объявляются совершенно независимо. В пределах текущего модуля имя глобальной переменной, естественно, должно быть уникальным. Областью действия глобальных переменных является по умолчанию вся программа.

Довольно интересная проблема возникает, казалось бы, когда имя локальной переменной функции совпадает с именем некоторой глобальной переменной. Это вполне допустимая ситуация, и одноименные переменные здесь на самом деле различны. Если мы входим внутрь определения функции, то оказываемся в области действия сразу двух переменных. Однако локальная переменная в этом случае скрывает, как говорят, глобальную переменную с тем же именем. Тут речь идет о их области видимости, которая не совпадает с областью действия. Эти два понятия часто путают. Вот пример кода, иллюстрирующий вышесказанное.

/***************************************************

** Область действия и видимость переменных.

*/

#include <stdio.h>

int iVar = 111; // Глобальная переменная.

void Funci(void)

{

int iVar = 222; // Локальная переменная Funci().

/* Локальная переменная скрывает глобальную. */

printf("Значение iVar в Func1() равно %d.\n", iVar);

}

void Func2(void)

{

/* Глобальная переменная доступна. */

printf("Значение iVar в Func2 () равно %d.\n", iVar) ;

iVar = 333; // Изменяет глобальную переменную.

}

int main(void)

(

printf ("Начальное значение iVar: %d.\n", -iVar) ;

// Печатает 111. Funci (); // Печатает 222, но не изменяет

// глобальную iVar.

printf("После вызова Func1(): %d.\n", iVar) ;

Func2 (); // Печатает 111 и изменяет iVar на 333.

printf ("После вызова F'unc2(): %d.\n", iVar) ;

return 0;

}

Время жизни

Время жизни переменной в известной мере определяется ее областью действия. Память под глобальную переменную отводится, можно сказать, еще на этапе компиляции программы; во всяком случае, переменная существует все время, пока программа выполняется.

Локальная переменная создается при входе в функцию и уничтожается при возврате из нее. Она является автоматической переменной. Поэтому никак нельзя ожидать, например, что локальная переменная будет сохранять свое значение в промежутках между вызовами объявляющей ее функции. Память под переменную выделяется на стеке программы.

Если же вы хотите, чтобы локальная переменная сохраняла значение между вызовами функции, ее следует объявить с модификатором static, как в следующем:

int AFunc(void)

{

/* Так можно организовать счетчик вызовов функции. */

static int callCount = 0;

// Здесь что-то делается...

return ++callCount;

}

На тот случай, если у кого-то этот код вызвал сомнения, скажу, что инициализация локальной статической переменной производится всего один раз — при запуске программы, а не при каждом входе в функцию.

Такая переменная будет существовать все время, пока программа выполняется. Память под статическую переменную отводится в той же области, где располагаются глобальные переменные. Таким образом, статическая локальная переменная очень похожа на глобальную, за исключением того, что ее областью действия является все-таки объявляющая функция; вне ее переменная недоступна.

Модификаторы переменных

Помимо static, в С имеются и другие модификаторы, применяемые к объявлениям переменных. Опишем их вкратце.

Модификатор volatile

Об этом модификаторе следует сказать отдельно. Он применяется для объявления переменных, которые можно назвать нестабильными. Модификатор volatile сообщает компилятору, что значение переменной может изменяться как бы само по себе, например, некоторым фоновым процессом или аппаратно. Поэтому компилятор не должен пытаться как-либо оптимизировать выражения, в которые входит переменная, — предполагая, например, что ее значение не менялось с предыдущего раза и потому выражение не нужно заново пересчитывать.

Есть и другой момент. В программе могут быть так называемые критические участки кода, во время исполнения которых изменение значения нестабильной переменной приведет к абсурдным результатам. (Возьмите случай оценки “А или не-А”, если А нестабильно!) Для таких критических участков компилятор должен создавать копию, например, в регистре, и пользоваться этой копией, а не самой переменной.

Можно написать такое объявление:

volatile const int vciVar = 10;

Другими словами, “нестабильная константа” типа int. В этом нет никакого противоречия — компилятор не позволит вашей программе изменять переменную, но и не будет предполагать ее значение известным априори, так как оно может меняться в силу внешних причин.

Управляющие конструкции С

Программа, операторы которой исполняются строго последовательно — так, как они записаны, — обладает весьма скромными возможностями. Именно такими и были приводимые нами до сих пор примеры. Настоящая программа должна уметь “принимать решения”, т. е. изменять последовательность исполнения своих операторов в зависимости от текущей ситуации — входных данных, результатов вычислений, сообщений Windows и т. д. Простейшим примером может служить выполнение каких-то операторов, если некоторое условие истинно, и пропуск их в противном случае.

С и Pascal называют языками структурного программирования', не столько потому, что в этих языках имеется понятие “структуры данных”, но в основном благодаря структурированному потоку управления. Что имеется в виду? Существуют всего три основных структуры потока управления (поток управления можно определить как алгоритм перехода от текущего оператора к следующему):

Из этих трех структур можно строить сколь угодно сложные управляющие конструкции, поскольку они подчиняются правилу суперпозиции, на место любого оператора некоторой структуры можно в свою очередь подставить любую структуру. При этом иногда последнюю требуется заключить в операторные скобки — в С это фигурные.скобки {} — подобно тому, как в арифметических выражениях используются обычные скобки. Любая последовательность операторов, заключенная в фигурные скобки, с точки зрения потока управления считается единым оператором.

Условный оператор if... else

Условный оператор реализует структуру выбора. Он имеет такой вид:

if (условие) оператор1 else оператор 2

Если условие оценивается как истинное (ненулевое), выполняется onepamop1, если как ложное (нулевое), выполняется onepamop2. Простейший пример:

if (а > b)

max_ab = a;

else

max_ab = b;

Как было сказано чуть выше, вместо одиночного оператора всегда можно подставить блок из нескольких операторов, заключенный в фигурные скобки. Другими словами, возможна следующая синтаксическая форма:

if (условие)

{опера торы_блока_if)

else

(опера торы_блока_еlsе}

В случае, когда при ложности условия не нужно выполнять никаких действий, а требуется только пропустить операторы блока if, ключевое слово else и соответствующий ему оператор (блок) могут отсутствовать, как в следующем примере:

if (а > b) { // Если а > b, поменять их местами;

temp = а; // в противном случае оставить все, как есть.

а = b;

b = temp;

}

//...Продолжение программы...

В соответствии с правилом суперпозиции можно строить вложенные структуры if...else, например:

if (a > b)

if (a > с)

max_abc = а;

else

max abc = с;

else

if (b > с)

max_abc = b;

else

max_abc = с ;

Эта конструкция всего-навсего определяет наибольшее из трех чисел, но разобраться в ее логике не так-то просто. Кроме того, следует помнить, что если во вложенных условных структурах используются как полные, так и неполные операторы if (без else), то могут возникать неоднозначности. Попробуем, например, переписать предыдущий фрагмент чуть более экономно, заранее присвоив максимуму значение с:

Max_abc = с;

if (a > b)

if (a > c)

max_abc == a;

else

if (b > C) max_abc = b;

К которому из двух первых if относится это else? По задуманной нами логике — к первому, однако компилятор считает по-другому; он разрешает подобные неоднозначности, ставя спорное else в соответствие ближайшему if, т. е. в данном случае второму. В результате все работает неправильно. Чтобы устранить неоднозначность, нужно применить операторные скобки:

max_abc = с;

if (а > b) {

if (а > с)

max_abc = а;

else if (b > с)

max abc = b;

Об условиях в операторе if

Условие оператора if может быть сколь угодно сложным выражением. Можно было бы сказать, что это выражение должно быть “логическим”, но в С нет логического типа данных. Как уже говорилось, выражение считается ложным, если его значением является нуль, и истинным, если значение ненулевое.

Вот несколько примеров условий оператора if:

if (x) DoSomething();

// Если х не равно

// нулю.

if (!x) DoAnotherThing();

// Если х равно нулю.

if (b == с) DoAnotherThing();

// Если b равно с.

if (b != с) DoSomething();

// Если b не равно с.

if ((key = getch()) == 'q') DoQuitO;

// Сохранить код

// клавиши в key

// и проверить, равен

// ли он ' q ' .

#define ERR_FLAG 0х80

// Если бит ERR_FLAG

if (flags & ERR_FLAG) ReportError();

// переменной flags

// установлен.

if (a >= b && a <= c) DoSomething();

// Если а лежит между

// b и с.

Операции отношения (==, !=, <, >= и т. д.) возвращают ненулевой целый результат, если значения операндов удовлетворяют отношению. В большинстве реализации С это 1, но полагаться на это не стоит. Если отношение не удовлетворяется, результатом операции будет нуль.

Обратите внимание на три последних примера. В пятом примере вы можете видеть разницу между присваиванием (=) и отношением равенства (= =). Не забывайте, что в С присваивание является операцией, возвращающей значение.

В шестом примере вы видите поразрядную операцию AND, с помощью которой проверяется состояние отдельного бита переменной flags. В седьмом примере применена логическая операция AND, которая логически перемножает значения двух отношений.

Кстати о флагах и поразрядных операциях. Битовые флаги — довольно распространенный и очень эффективный прием хранения и передачи информации о состоянии какого-то объекта или процесса, хотя и не очень безопасный. Вот примеры манипуляций с флагами:

flags | = ERR_FLAG; // Установка флага операцией OR.
flags &= ~ERR_FLAG; // Сброс флага операцией AND.
flags "= ERR_FLAG; // Переключение флага операцией XOR.

Оператор выбора switch

Часто возникают ситуации, когда некоторая переменная может принимать несколько возможных значений-вариантов, и для каждого варианта требуется выполнить какие-то свои действия. Например, пользователю может быть предложено меню, когда нажатие различных клавиш инициирует соответствующие команды. Управляющая конструкция, реализующая такую логику, может использовать “последовательно вложенные” операторы if...else if...:

int key;

printf("\nSelect command (F, M or Q): ");

// Вывести подсказку. key = getch();

// Прочитать символ. key = toupper(key);

// Преобразовать в верхний регистр. if (key == 'F')

// Определение команды...

printf("\n\"F\" selected - means File.\n");

else if (key == 'M')

printf("\n\"M\" selected - means Message.\n");

else if (key == 'Q')

printf("\n\"Q\" selected - means Quit.\n");

else

printf("\nlnvalid key!");

Здесь мы применили функцию преобразования символа в верхний регистр toupper (), чтобы можно было вводить букву команды в обоих регистрах (например, F или f).

Условия операторов if содержат проверку кода нажатой клавиши на равенство одному из допустимых символов. Если код клавиши не соответствует никакой команде, выводится сообщение об ошибке.

Для подобных случаев в С существует специальная конструкция выбора switch. Выглядит она так:

switch (выражение)

{

case константное_выражение: группа_операторов case константное_выражение: группа_операторов

[default: группа операторов] }

Сначала производится оценка выражения в операторе switch; полученное значение последовательно сравнивается с каждым из константных_выражений, и при совпадении значений управление передается на соответствующую группу_операторов. Если значение выражения не подходит ни под один из вариантов, управление передается на группу операторов с меткой default или на следующий после блока switch оператор, если группа default отсутствует.

Под группой _операторов подразумевается просто один или несколько произвольных операторов. Группа здесь вовсе не обязана быть блоком, т. е. заключать ее в операторные скобки не требуется.

И еще одна особенность, о которой следует помнить при написании структур switch. Если найдена метка case, совпадающая со значением проверяемого выражения, то выполняется группа_операторов данного case. Однако дело на этом не заканчивается, поскольку, если не принять никаких дополнительных мер, управление “провалится” ниже, на следующую по порядку метку case и т. д., и в результате будут выполнены все операторы до самого конца блока switch. Если это нежелательно (как чаще всего и бывает), в конце группы_операторов case нужно поставить оператор break. Он прерывает выполнение блока switch и передает управление оператору, непосредственно следующему за блоком.

Для иллюстрации мы перепишем предыдущий пример “меню”, оформив его в виде законченной программы, и продемонстрируем попутно еще один управляющий оператор С.

Листинг 3.3. Демонстрация структуры switch

/*

** Switch.с: Оператор выбора.

*/

#pragma hdrstop

#include <stdio.h>

#include <conio.h>

#pragma argsused

int rriain(int argc, char* argv[])

{

int key;

loop:

printf("\nEnter command (F, M or Q): ");

key = getche(); // Прочитать клавишу.

switch (key) ( // Определение команды... case 'f':

case 'F":

printf("\n\"File\" command selected.\n");

break;

case 'm':

case 'M':

printf ("\n\"Mess.age\" command selected.\n");

break;

case 'q':

case 'Q':

printf("\n\"Quit\" command selected.\n");

printf("\nPress a key to Exit...");

getch() ;

return 0; // Возврат в Windows. default:

printf("\nlnvalid command!\n") ;

}

goto loop; // Следующая команда.

}

Рис. 3.3 Программа Switch

Мы организовали в программе простейший “бесконечный” цикл, который все время просит пользователя ввести команду — до тех пор, пока не будет нажата клавиша “Q”. В этом случае происходит возврат в операционную систему.

Чтение команды производится функцией getche(). Она, как и getch (), возвращает код нажатой клавиши, однако в отличие от getch () отображает введенный символ на экране.

Для реализации цикла мы использовали архаический оператор goto, исполнение которого приводит к передаче управления на метку, указанную в операторе. В примере метка с именем loop стоит в самом начале программы. Таким образом, дойдя до конца функции main (), управление возвращается к ее началу и все повторяется снова.

Как я сказал, оператор goto весьма архаичен, и он, конечно, никак не укладывается в концепцию структурированного потока управления. Единственным случаем, когда его использование в программе на С может быть оправдано, является обработка некоторых аварийных ситуаций. Если имеется очень сложная система вложенных друг в друга управляющих структур и ошибка происходит где-то глубоко внутри, то проще и надежнее всего передать управление на процедуру обработки ошибок с помощью goto.

Циклы

В языке С структуры многократного повторения реализуются тремя разновидностями операторов цикла. Это циклы while, do... while и for.

Цикл while

Синтаксис оператора while выглядит так:

while (условие продолжения) оператор

Сначала оценивается условие_продолжения. Если оно истинно, выполняется оператор, после чего управление возвращается заголовку цикла, и все повторяется снова. Когда условие оказывается ложным, управление передается следующему после цикла оператору.

Как обычно, одиночный оператор тела цикла можно заменить блоком, заключенным в фигурные скобки:

whi1е (условие_продолжения)

{

операторы_тела цикла

}

Используя этот оператор, можно было бы переписать предыдущий пример (со структурой switch) следующим образом:

int main(int argc, char* argv[ ])

{

int key, done = 0;

while (!done) {

printf("\nEnter command (F, M or Q): ");

key = getche(); // Прочитать клавишу.

switch (key) ( // Определение команды... case 'f':

case 'F':

printf("\n\"File\" command selected.\n");

break;

case 'm':

case 'M':

printf("\n\"Message\" command selected.\n");

break;

case 'q':

case 'Q':

printf("\n\"Quit\" command selected.\n");

done = 1; // Завершить цикл.

break; default:

printf("\nlnvalid command!\n") ;

}

} printf("\nPress a key to Exit...");

getch() ;

return 0; // Возврат в Windows.

}

Это более “грамотная” версия цикла обработки команд. Пока done равняется нулю, цикл продолжает выполняться. Когда нажимают клавишу 'q', done присваивается единица и при очередной оценке условия оно оказывается ложным; цикл завершается.

Обратите внимание, что в цикле while проверка условия делается перед выполнением тела цикла. Если условие изначально ложно, то тело цикла не исполняется вообще, ни одного раза.

Цикл do—while

Этот цикл имеет такой вид:

do оператор while (условие продолжения);

Здесь сначала выполняется оператор, а затем производится проверка условия_продолжения. Если условие истинно, управление возвращается в начало цикла; если ложно, цикл завершается и управление переходит к оператору, следующему за циклом.

Отличие от предыдущей конструкции очевидно — тело цикла do... while исполняется хотя бы один раз вне зависимости от каких-либо условий, в то время как в цикле while при ложном условии тело не исполняется вообще. Хотя конкретную программную задачу можно решить, применив любую из этих конструкций, чаще всего одно из двух решений оказывается более экономным.

Цикл for

Цикл for, наиболее универсальный из всех циклов языка С, выглядит так:

for ([инициализация]; [условие]; [модификация]) оператор

Прежде всего выполняется инициализация цикла; секция инициализации может содержать любое выражение. Инициализация производится только один раз перед началом работы цикла.

Оценивается выражение условия. Если оно истинно, выполняется оператор тела цикла; если условие ложно, происходит выход из цикла и управление передается следующему оператору.

После исполнения тела цикла производится модификация, после чего управление возвращается заголовку цикла и все повторяется снова. Секция модификации может содержать любое выражение; обычно в ней изменяют значения управляющих переменных цикла.

Как видно из синтаксического описания, любую секцию заголовка цикла for можно опустить, но разделители — точки с запятой — все равно должны присутствовать. Если опущено условие, цикл будет выполняться бесконечно.

Простейшей и самой популярной конструкцией на основе цикла for является цикл с управляющей переменнои-счетчиком:

int i;

for (i =0; i < REPEAT; i++)

DoSomething (i);

Счетчик инициализируется значением 0. В начале каждого прохода цикла проверяется, не достиг ли он значения REPEAT. Как только i станет равным REPEAT, тело цикла пропускается и управление передается следующему оператору. В конце каждого прохода i увеличивается на единицу. Как нетрудно подсчитать, тело цикла выполняется для значений i от О до REPEAT-1, т. е. REPEAT раз.

Любую конкретную структуру повторения, требуемую для решения некоторой задачи, можно реализовать на основе любого из циклов С, однако всегда какой-то из них подходит к данному случаю наилучшим образом, позволяя написать более ясный и компактный код. Так, если необходимое число итераций цикла известно заранее (как при обработке массива), проще всего применить цикл for. Если же число итераций заранее определить нельзя, как в нашем примере обработки команд (момент завершения цикла определяется пользователем) или при операциях поиска в списке, применяют цикл while или do. . .while.

У структур повторения в ряде ситуаций есть альтернатива. Это рекурсия, заключающаяся в том, что функция вызывает саму себя. Естественно, такой вызов должен быть условным, т. е. обязательно должен наступить такой момент, когда на очередном шаге рекурсивного вызова не происходит. Есть классический пример рекурсии — вычисление факториала:

unsigned Fac(unsigned n)

{

if (n)

return n * Fac(n - 1);

else

return 1;

}

Когда аргумент в очередном вызове оказывается равен 0, рекурсия завершается — функция возвращает 1. До этого момента не происходило, по существу, реальных вычислений (умножений). На стеке накапливались вызовы Fac () с последовательно уменьшающимися аргументами. Теперь стек начинает “разматываться”, и возвращаемые на каждом шаге значения умножаются на последовательно увеличивающиеся n. Глубина рекурсии ограничивается только размером стека программы.

Факториал, конечно, вообще никто никогда не вычисляет, во всяком случае, подобным образом. Однако существуют задачи, для которых метод рекурсии оказывается естественным решением и реализуется намного проще, чем это можно

было бы сделать с помощью циклов. Таковы, прежде всего/ различные операции над древовидными структурами данных.

Не все языки программирования допускают рекурсию. Мы напомнили о ней просто для того, чтобы подчеркнуть, что С — один из тех языков, в которых рекурсия возможна.

Операторы прерывания блока

Часто бывает необходимо “досрочно” выйти из некоторого цикла, до того, как будет удовлетворено условие его завершения (говоря точнее, до того, как условие продолжения станет ложным). Например, вы просматриваете массив на предмет поиска заданного значения. Как только нужный элемент массива найден, выполнять цикл далее нет необходимости. Для досрочного завершения циклов в С применяются операторы break, return и continue. С оператором break вы уже встречались — помимо циклов, он используется в блоках switch.

Разумеется, операторы прерывания циклов должны выполняться условно, т. е. должны входить в блок if или else некоторого условного оператора и исполняться только при наступлении условий досрочного завершения цикла.

Эти два оператора эквивалентны следующим конструкциям с goto:

// Эквивалент break:

while (...) {

goto brkLabel;

} // Закрывающая скобка блока. brkLabel:

// Метка следующего за блоком оператора.

// Эквивалент continue:

while (...) (

goto cntLabel;

CntLabel:; // Пустой помеченный оператор.

} // Закрывающая скобка блока.

Использование break и continue для прерывания циклов, вообще говоря, нежелательно — по тем же причинам, что и использование goto (от операторов break в структуре выбора switch никуда не уйти). Они нарушают структурную организацию потока управления и затрудняют чтение текста программы.

return [выражение];

Если функция “возвращает” тип void, выражение опускается. Если код функции при ее вызове не исполняет ни одного оператора return, подразумевается, что return присутствует в качестве последнего оператора тела функции. Возвращаемое значение при этом не определено.

Не следует ставить операторы return где попало. Вернитесь и посмотрите на листинг 3.3. Это пример того, как не надо программировать. По правилам “хорошего тона” оператор возврата должен быть только один (от силы два) и он должен располагаться в конце тела функции.

Блоки и локальные переменные

Поскольку при описании управляющих конструкций мы попутно ввели понятие блока, нужно сделать одно уточнение касательно объявлений и области действия локальных переменных. На самом деле локальные переменные могут объявляться не только в начале тела функции, но и в любом другом блоке (if, while и т. д.). Областью действия переменной является блок, в котором она объявлена; она скрывает любую переменную с тем же именем, объявленную вне данного блока. За пределами блока переменная недоступна.

Массивы и указатели

Массивы и указатели довольно тесно связаны между собой. Имя массива можно разыменовывать, как указатель. В свою очередь, указатель можно индексировать, как массив, если это имеет смысл. Поэтому мы рассматриваем массивы и указатели в одном разделе.

Массивы

Массив по существу является совокупностью однотипных переменных (элементов массива), объединенных под одним именем и различающихся своими индексами. Массив объявляется подобно простой переменной, но после имени массива указывается число его элементов в квадратных скобках:

int myArray[8];

Массив, как и переменную, можно инициализировать при объявлении. Значения для последовательных элементов массива отделяются друг от друга запятыми и заключаются в фигурные скобки:

int iArray[8] = {7, 4, 3, 5, 0, 1, 2, 6);

Обращение к отдельным элементам массива производится путем указания индекса элемента в квадратных скобках, например:

myArray[3] = 11;

myArray[i] = iArray[7-i];

Индекс должен быть целым выражением, значение которого не выходит за пределы допустимого диапазона. Поскольку индексация массивов начинается в С всегда с нуля (т. е. первый элемент имеет индекс 0), то, если массив состоит из N элементов, индекс может принимать значения от О до N-1.

В языке С не предусмотрена автоматическая проверка допустимости значений индекса времени выполнения, поэтому при индексации массивов нужно быть внимательным. Выход индекса за границы массива может приводить к совершенно непредсказуемым результатам.

Массивы естественным образом сочетаются с циклами for. Мы приведем пример программы, работающей с массивом целых чисел. Она выполняет так называемую “пузырьковую сортировку” введенных пользователем чисел в порядке возрастания. Работу программы иллюстрирует рис. 3.4.

Листинг 3.4. Программа пузырьковой сортировки

/*

** Loop.с: Программа пузырьковой сортировки.

*/

#pragma hdrstop

#include <stdio.h>

#include <stdlib.h>

#include <conio.h>

/**********************************************

** Процедура сортировки

*/

void DoSort(int array[ ], int n)

{

int i, j, temp;

for (i = n-1; i > 0; i--)

for (j = 0; j < i; j++)

if (array[j] > array[j+l]) {

temp = array[j];

array[j] = array[j+l];

array [j+1] = temp;

}

} /* Конец DoSortO */

#pragma argsused

int main(int argc, char* argv[])

{

const int N = 8;

int i, iArray[8];

char s[80], *endPtr;

printf("Enter %d integers separated by spaces:\n", N);

gets(s); // Прочитать строку пользователя.

endPtr = s; // Инициализировать указатель строки.

for (i =0; i < N; i++) // Преобразование чисел.

iArray[i] = strto1(endPtr, &endPtr, 10);

DoSort(iArray, N); // Вызов программы сортировки.

printf("Sorted array:\n");

for (i =0; i < N; i++)

// Вывод отсортированного

// массива. printf("%8d ", iArray[i]);

printf("\n\nPress a key...");

getch() ;

return 0;

}

Как видите, функция DoSort (), выполняющая сортировку массива из целых чисел, содержит двойной вложенный цикл for. Операторы внутри цикла обменивают значения двух соседних элементов массива, если первый из них больше второго. В главной функции циклы for использованы для чтения и вывода элементов массива iArray.

Строка, содержащая все введенные пользователем числа, считывается целиком в символьный массив s. После этого для преобразования символьного представления чисел в данные целого типа в цикле вызывается функция strtol (). Ее прототип (в stdlib.h) имеет вид

long strtol(const char *str, char **endptr, int radix);

Эта функция действует подобно atol (), преобразуя строку str в значение типа long, однако обладает более широкими возможностями. Параметр radix задает основание системы счисления (8, 10 или 16). В параметре endptr функция возвращает указатель на непреобразованную часть строки str (т. е. на строку, оставшуюся после исключения из нее первого из чисел). Таким образом, в цикле мы последовательно вычленяем из строки все восемь чисел и записываем их в элементы целого массива.

Функция позволяет довольно просто контролировать корректность ввода. Если при ее вызове происходит ошибка, например, строка содержит меньше чисел, чем их должно быть, возвращаемый в endptr указатель будет совпадать с аргументом str.

Ключевое слово const в объявлении первого параметра говорит компилятору, что функция не должна изменять элементы строки, на которую указывает str. Попытка модифицировать строку в теле функции вызовет ошибку при ее компиляции.

Рис. 3.4 Робота программы сортировки

Указатели

Указатель — это переменная, которая содержит адрес другого объекта. Этим объектом может быть некоторая переменная, динамический объект или функция. Говорят, что указатель ссылается на соответствующий объект. Хотя адрес, по существу — 32-битное целое число, определяющее положение объекта в виртуальной памяти программы, указатель является не просто целым числом, а специальным типом данных. Он “помнит”, на какого рода данные ссылается. Объявление указателя выглядит так:

тип_указываемого_объекта *имя_указателя [= значение];

Вот примеры объявлений:

int *pIntVar; // Указатель на целое.

double *pDouble = SdoubleVar; // Инициализация указателя

// на double.

char *arrStr[16]; // Массив указателей на char.

char (*arrStr) [16][16]; // Указатель на матрицу char.

Последний пример довольно специфичен. Подобные конструкции применяются в объявлении параметров функций, передающих многомерные массивы неопределенного размера.

 

Чтобы получить доступ к объекту, на который указатель ссылается. последний разыменовывают, применяя операцию-звездочку. Например. *pDouble будет представлять значение переменной, на которую ссылается

pDouble:

double doubleVar = 3.14159265;

double *pDouble = SdoubleVar;

printf("Значение самого указателя (адрес): %р", pDoubie) ;

printf("Число, на которое он ссылается: %f", *pDouble);

Как мы уже говорили в предыдущих разделах, указатели используются при обработке строк, а также для передачи функциям параметров, зна-

чения которых могут ими изменяться (передача по ссылке). Но главная “прелесть” указателей в том, что они позволяют создавать и обрабатывать динамические структуры данных. В языке С можно выделить память под некоторый объект не только с помощью оператора объявления, но и динамически, во время исполнения программы. Объект создается в свободной области виртуальной памяти функцией та 11 о с (). Вот пример (предполагается, что переменные объявлены так, как выше):

pDouble = malice(sizeof(double)); // Динамическое выделение

// памяти. *pDouble = doubleVar; // Присвоение значения

// динамическому объекту.

printf("Значение динамического объекта: %f", *pDouble) ;

free(pDouble); // Освобождение памяти.

Аргументом malloc () является размер области памяти, которую нужно выделить; для этого можно применить операцию sizeof, которая возвращает размер (в байтах) переменной или типа, указанного в качестве операнда.

Функция malloc () возвращает значение типа void* — “пустой указатель”. Это указатель, который может указывать на данные любого типа. Такой указатель нельзя разыменовывать, поскольку неизвестно, на что он указывает — сколько байтов занимает его объект и как их нужно интерпретировать. В данном случае операция присваивания автоматически приводит значение malloc () к типу double*. Можно было бы написать в явном виде

pDouble = (double*)malloc(sizeof(double));

Если выделение памяти по какой-то причине невозможно, malloc () возвращает NULL, нулевой указатель. На самом деле эта константа определяется в stdlib.h как целое — “длинный нуль”:

#define NULL OL

Хорошо ли вы поняли смысл различия двух последних примеров? В первом из них указателю pDouble присваивается адрес переменной doubleVar. Во втором указателю присваивается адрес динамически созданного объекта типа double;

после этого объекту, на который ссылается pDouble, присваивается значение переменной doubleVar. Создается динамическая копия значения переменной.

Память, выделенную malloc (), следует освободить функцией free () , если динамический объект вам больше не нужен. Возьмите это себе за правило и не полагайтесь на то, что система Windows автоматически уничтожает все динамические объекты программы по ее завершении.

Перед тем, как разыменовывать указатель, его нужно обязательно инициализировать, либо при объявлении, либо путем присвоения ему адреса какого-либо объекта, возможно, динамического — как в последнем примере. Аналогично, если к указателю применяется функция free(), он становится недействительным и не ссылается больше ни на какие осмысленные данные. Чтобы использовать его повторно, необходимо снова присвоить ему адрес некоторого объекта.

Разыменование нулевого указателя также приводит к ошибке. Поэтому при работе с объектами, создаваемыми с помощью malloc (), обычно всегда проверяют, не возвратила ли эта функция нулевое значение.

Указатель на функцию

Можно объявить, инициализировать и использовать указатель на функцию. В вызовах API Windows часто применяют, например, “возвратно-вызываемые функции”. В вызове API в качестве аргумента в этом случае употребляется указатель на соответствующую функцию.

Вот пример, из которого все станет ясно.

/**********************************************

** Некоторая функция:

*/

void ShowString(char *s)

{

printf (s);

}

/***********************************************

** Главная функция:

*/

int main(void) {

void (*pFunc)(char*); // Объявление указателя на функцию.

pFunc = ShowString; // Инициализация указателя адресом

// функции. (*pFunc)("Calling a pointer to function!\n");

return 0;

}

Вы, возможно, обратили внимание, что в примере указателю присваивается значение, представленное просто именем функции без скобок со списком параметров. То есть “значение”, представленное именем функции, имеет тот же тип, что и объявленный здесь указатель. Поэтому и вызвать функцию через указатель можно было бы проще:

pFunc("Calling a pointer to function!\n");

Соотношение между указателями и функциями примерно такое же, как между указателями и массивами, о чем говорится в следующем разделе.

Указатели и массивы

Между указателями и массивами в С существует тесная связь. Имя массива без индекса эквивалентно указателю на его первый элемент. Поэтому можно написать:

int iArray[4] ;

int *piArr;

piArr = iArray; // piArr указывает на начальный элемент // iArray.

Последнее эквивалентно

piArr = &iArray[0];

И наоборот, указатель можно использовать подобно имени массива, г. е. индексировать его. Например, piArr [3] представляет четвертый элемент массива iArray [ ] .

К указателю можно прибавлять или отнимать от него целочисленные выражения, применять операции инкремента и декремента. При этом значение указателя изменяется в соответствии с размером объектов, на которые он указывает. Так, (piArr + 2) указывает на третий элемент массива. Это то же самое, что и & iArray [ 2 ]. Когда мы прибавляем к указателю единицу (piArr++) , адрес, который в нем содержится, в действительности увеличивается на 4 — размер типа int.

Короче говоря, в выражениях с указателями и массивами можно обращаться одинаково. Следует только помнить, что объявление массива выделяет память под соответствующее число элементов, а объявление указателя никакой памяти не выделяет, вернее, выделяет память для хранения значения указателя — некоторого адреса. Компилятор по-разному рассматривает указатели и массивы, хотя внешне они могут выглядеть очень похоже.

Однако возможности, которые раскрываются перед программистом благодаря указателям, выявляются в полной мере лишь при работе с динамическими структурами данных, о которых мы поговорим в следующем разделе.

Типы, определяемые пользователем

Встроенные типы данных, указатели и массивы образуют основу для представления и обработки информации на языке С. Подлинная же сила языка состоит в том, что он позволяет пользователю (под “пользователем” в подобного рода выражениях понимается программист) самому определять наиболее подходящие для конкретной задачи типы, способные адекватно представлять сложно структурированные данные реального мира. Эти средства С мы начнем изучать со сравнительно простого вспомогательного оператора typedef.

Переименование типов

Любому типу в С можно присвоить простое имя или переименовать его. Это делается с помощью ключевого слова typedef:

typedef тип новое_имя_типа;

или

typedef тип новое_имя_типа [размер_массива][...];

для типов-массивов. (Квадратные скобки здесь означают не необязательность синтаксического элемента, а “настоящие” скобки.) Кроме того, мож-

но вводить имена для типов указателей на функцию и т. п. Формально описать все возможные typedef довольно сложно, поэтому мы этого делать не будем. Вообще следует руководствоваться таким правилом: если вы объявляете объект как принадлежащий к определенному в typedef типу, имя объекта нужно подставить вместо нового_имени_типа. Убрав typedef, вы получите эквивалентное объявление объекта. Вот примеры:

typedef short Arrlndex;

// Псевдоним для short.

typedef char MessageStr[80];

// Имя типа для массивов

// char[80].

typedef int *IPtrFunc(void);

// Функция, возвращающая

// указатель на int.

typedef int (*IFuncPtr)(void);

// Указатель на функцию,

// возвращающую int.

В общем, typedef является просто средством упрощения записи операторов объявления переменных.

Перечислимые типы

Ключевое слово enum позволяет описать перечислимый тип, представляющий переменные, которые могут принимать значения из заданного набора целых именованных констант. Определение перечислимого типа выглядит так:

enum имя-этикетка {имя_константы [= значение], ...};

Значение равно по умолчанию нулю для первого из перечислителей (так обычно называют определяемые в enum константы). Любая другая константа, для которой значение не указано, принимается равной значению предыдущей константы плюс единица.

Например:

enum Status

{

Success = 1,

Wait, Proceed,

Error = -1

};

В операторе enum после закрывающей фигурной скобки можно сразу объявить несколько переменных данного типа:

enum этикетка {список_констант} переменная[, ...];

Нужно иметь в виду, что имя-этикетка не является настоящим именем типа. Именем типа будет в вышеприведенном примере enum Status. Соответственно переменные должны объявляться как

enum Status ProclStatus, Proc2Status;

Однако всегда можно воспользоваться ключевым словом typedef и ввести для перечисления подлинное новое имя. Обычно это делается сразу:

typedef enum этикетка {список_констант) имя_типа;

Предыдущее объявление можно переписать так:

typedef enum _Status {

Success = 1,

Wait, Proceed,

Error = -1 } Status;

Тогда Status будет полноценным именем перечислимого типа. (Обратите внимание, что для этикетки мы указали имя _Status. Это обычная практика.)

Структуры

Массивы позволяют обращаться с набором логически связанных однотипных элементов как с единым целым. Если же требуется хранить набор разнородных, но логически связанных данных, описывающих, например, состояние некоторого объекта реального мира, используются структуры. Синтаксис структуры имеет такой вид:

struct этикетка [список элементов] [переменные];

Список_элементов состоит из объявлений, аналогичных объявлениям переменных. Объявления элементов оканчиваются точкой с запятой. Вот простой пример структуры, предназначенной для хранения основных сведений о человеке:

struct Person (

char lastName[32]; // Фамилия.

char firstName[32]; // Имя.

Sex sex;

// Пол: перечислимый тип

// (male, female}.

short age; // Возраст.

long phoneNum; // Телефон как длинное целое.

}

aPerson; // Объявляет переменную типа

// struct Person.

Как видите, все довольно просто. Структура группирует различные данные, относящиеся к конкретному человеку. Как и в случае перечислений, в определении структуры можно сразу объявить переменные структурного типа, указав их имена после закрывающей фигурной скобки. Аналогично именем типа является struct этикетка, и его можно сразу переопределить с помощью ключевого слова typedef.

Для доступа к отдельным элементам структуры имеются две операции: точка и стрелка, за которыми следует имя элемента. Какую из них следует применять, зависит от того, имеете ли вы дело с самой переменной-структурой или у вас есть только указатель на нее, как это имеет место в случае динамических объектов. С именем переменной применяется точка, с указателем — стрелка. Имея в виду предыдущее определение, можно было бы написать:

struct Person *pPerson - SaPerson;

// Указатель на

// структуру.

aPerson.age = atol(ageStr);

// Записать в структуру

// возраст aPerson.sex - male;

// и т.д.

pPerson->phoneNum = atol(phoneStr); //

/* Напечатать имя и фамилию (предполагается, что они уже инициализированы).*/

printf("%s %s\n", pPerson->firstName, pPerson->lastName);

Битовые поля

В качестве элементов структуры можно определять битовые поля. Для них задается ширина поля в битах, и компилятор отводит под элемент ровно столько бит, сколько указано. Несколько битовых полей может быть таким образом упаковано в одном слове. Синтаксис битового поля:

тип [имя поля]: ширина поля;

Тип поля может быть int или unsigned int. Доступ к битовым полям осуществляется так же, как и к регулярным элементам структуры. Если имя_поля отсутствует, место под поле отводится, но оно остается недоступным. Это будут просто “заполняющие” биты.

Битовые поля применяются либо там, где необходима плотная упаковка информации (как это бывает при передаче функции некоторого набора логических флагов), либо, например, для отображения регистров внешнего устройства, которые часто бывают организованы как совокупность небольших полей и отдельных битов.

Объединения

Объединения, определяемые с помощью ключевого слова union, похожи по своему виду на структуры:

union этикетка {список_элементов] [переменные];

Отличие состоит в том, что все элементы объединения занимают одно и то же место в памяти, они перекрываются. Компилятор отводит под объединение память, достаточную для размещения наибольшего элемента.

Объединения полезны, когда требуется обеспечить своего рода “полиморфное поведение” некоторого объекта. Например, вы хотите определить тип, реализующий представление различных геометрических фигур — прямоугольников, окружностей, линий, многоугольников. В зависимости от того, чем конкретно является данная фигура, для ее описания необходимы различные наборы значений. Круг описывается иначе, чем многоугольник и т. п. И вот из структур, описывающих различные фигуры, можно в свою очередь составить обобщенный тип-объединение, который будет обрабатываться различно в зависимости от значения специального поля, определяющего род фигуры. В то же время на все фигуры можно будет ссылаться через указатели одного и того же типа, что, в частности, позволит составлять динамические связанные списки из любых фигур.

Доступ к элементам объединения (их иногда называют разделами) осуществляется так же, как и в структурах, — посредством точки или стрелки, за которыми следует имя раздела.

В заключение разговора о типах, определяемых пользователем, приведем пример законченной программы. В ней определяется тип структуры, способной хранить данные различных “графических объектов”. В программе реализованы всего два их вида — прямоугольник и текстовая метка.

Листинг 3.5. Демонстрация работы со структурами

/*

** Struct.с: Структуры и объединения.

*/

#pragma hdrstop

#include <stdio.h>

#include <conio.h>

#include <string.h>

/* Тип для определения вида объекта */

typedef enum {Rect=l, Labi) Type;

/***********************************************

** Структура для хранения прямоугольников и текстовых меток.

*/ typedef struct _GForm { Type type;

struct _GForm *next;

/* Указатель для связанного списка. */

union {

/* Анонимное объединение. */

struct {

/* Прямоугольник. */

int left, top;

int right, bottom;

} rect;

struct {

/* Текстовая метка. */

int x, у;

char text [20];

} labi;

}

} Gform;

 

/****************************************

** Функция вывода данных объекта.

*/ void ShowForm(GForm *f)

{

switch (f->type) {

case Rect:

/* Прямоугольник. */

printf("Rectangle: (%d, %d) (%d, %d)\n",

f->data.rect.left, f->data.rect.top,

f->data.rect.right, f->data.rect.bottom);

break;

case Labi: /* Метка. */

printfC'Text label: (%d, %d) \"%s\"\n",

f->data.labl.x, f->data.labi.y, f->data.labi.text);

}

int main(void)

{

GForm formi, form2;

/* Инициализация первого объекта. */

forml.type = Rect;

forml.data.rect.left = 50;

forml.data.rect.top = 25;

forml.data.rect.right = 100;

forml.data.rect.bottom = 75;

/* Инициализация второго объекта. */

form2.type = Labi;

form2.data.labi.x = 60;

form2.data.labl.у = 40;

strcpy(form2.data.labi.text, "This is a Label!");

/* Распечатка... */ ShowForm(&formi);

ShowForm(&form2);

printf("\nPress any key...");

getch() ;

return 0;

}

Работу программы иллюстрирует рис. 3.5.

Рис. 3.5 Программа Struct.c (Project2)

Обратите внимание, что перечисления, структуры и объединения могут быть анонимными, т. е. не иметь имен-этикеток.

Внимательно рассмотрите определение типа Gform:

typedef struct _GForm { Type type;

struct GForm *next;

/* Указатель для связанного списка. */

union {

/* Анонимное объединение. */

struct {

/* Прямоугольник. */

int left, top;

int right, bottom;

} rect;

struct {

/* Текстовая метка. */

int x, у;

char text[20] ;

} labl;

} data;

} GForm;

Структура _Gform имеет, как таковая, три элемента: type, next (не используется) и data. Последний является анонимным объединением разделов rect и labl, каждый из которых, в свою очередь, является анонимной структурой. Элементы первой хранят значения координат верхнего левого и правого нижнего углов прямоугольника; элементами второй являются координаты, задающие положение текста, и сама текстовая строка. Получаются довольно длинные выражения для доступа к элементам данных (forml.data.rect.bottom).

Заключение

В этой весьма объемистой главе мы рассмотрели чуть ли не весь стандартный ANSI С, если не считать препроцессора. Препроцессором мы займемся в следующей главе.

Кроме того, вам предстоит еще познакомиться с расширениями С, поддерживаемыми C++Builder, но выходящими за рамки стандарта ANSI. Такие расширения, связанные в основном с особенностями процессора и операционной системы, почти всегда имеются в любой реализации языка.

О них также будет говориться в следующей главе.